
/**
  ******************************************************************************
  * Copyright 2021 The Microbee Authors. All Rights Reserved.
  * 
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  * 
  * http://www.apache.org/licenses/LICENSE-2.0
  * 
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * 
  * @file       gcs_ftp.c
  * @author     baiyang
  * @date       2023-3-12
  ******************************************************************************
  */

/*----------------------------------include-----------------------------------*/
#include "gcs.h"

#include <float.h>
#include <string.h>
#include <unistd.h>

#ifdef RT_USING_DFS
#include <dfs_fs.h>
#endif

#include <rtthread.h>
#include <fcntl.h>
#include <sys/errno.h>

#include <common/time/gp_time.h>
#include <common/gp_math/gp_mathlib.h>
#include <board_config/board_config.h>
#include <common/utility/sparse-endian.h>
/*-----------------------------------macro------------------------------------*/
#if MB_GCS_FTP_ENABLED

// timeout for session inactivity
#define FTP_SESSION_TIMEOUT 3000
/*----------------------------------typedef-----------------------------------*/

/*---------------------------------prototype----------------------------------*/
bool ftp_init();
bool gcsmav_send_ftp_reply(gcs_mavlink_t gcsmav, const struct pending_ftp *reply);
void gcsmav_ftp_error(gcs_mavlink_t gcsmav, struct pending_ftp *response, enum MAV_FTP_ERROR error);
void gcsmav_ftp_push_replies(gcs_mavlink_t gcsmav, struct pending_ftp *reply);
int gcsmav_gen_dir_entry(char *dest, size_t space, const char *path, const struct dirent * entry);
void gcsmav_ftp_list_dir(gcs_mavlink_t gcsmav, struct pending_ftp *request, struct pending_ftp *response);
/*----------------------------------variable----------------------------------*/
struct ftp_state gcs_ftp = {.fd = -1,
                            .reply = {.session = -1}};
/*-------------------------------------os-------------------------------------*/

/*----------------------------------function----------------------------------*/
bool ftp_init()
{
    // check if ftp is disabled for memory savings
    if (brd_ftp_disabled()) {
        goto failed;
    }

    return true;

failed:
    gcs_send_text(MAV_SEVERITY_WARNING, "failed to initialize MAVFTP");
    return false;
}

void gcsmav_handle_file_transfer_protocol(gcs_mavlink_t gcsmav, const mavlink_message_t *msg)
{
    if (!ftp_init()) {
        return;
    }

    mavlink_file_transfer_protocol_t packet;
    mavlink_msg_file_transfer_protocol_decode(msg, &packet);

    gcs_ftp.request.chan = gcsmav->chan;
    gcs_ftp.request.seq_number = le16toh_ptr(packet.payload);

    gcs_ftp.request.session = packet.payload[2];
    gcs_ftp.request.opcode = (enum MAV_FTP_OP)(packet.payload[3]);
    gcs_ftp.request.size = packet.payload[4];
    gcs_ftp.request.req_opcode = (enum MAV_FTP_OP)(packet.payload[5]);
    gcs_ftp.request.burst_complete = packet.payload[6];
    gcs_ftp.request.offset = le32toh_ptr(&packet.payload[8]);
    gcs_ftp.request.sysid = msg->sysid;
    gcs_ftp.request.compid = msg->compid;
    rt_memcpy(gcs_ftp.request.data, &packet.payload[12], sizeof(packet.payload) - 12);

    bool skip_push_reply = false;

    // if it's a rerequest and we still have the last response then send it
    if ((gcs_ftp.request.sysid == gcs_ftp.reply.sysid) && (gcs_ftp.request.compid == gcs_ftp.reply.compid) &&
        (gcs_ftp.request.session == gcs_ftp.reply.session) && (gcs_ftp.request.seq_number + 1 == gcs_ftp.reply.seq_number)) {
        gcsmav_ftp_push_replies(gcsmav, &gcs_ftp.reply);
        return;
    }

    // setup the response
    rt_memset(&gcs_ftp.reply, 0, sizeof(gcs_ftp.reply));
    gcs_ftp.reply.req_opcode = gcs_ftp.request.opcode;
    gcs_ftp.reply.session = gcs_ftp.request.session;
    gcs_ftp.reply.seq_number = gcs_ftp.request.seq_number + 1;
    gcs_ftp.reply.chan = gcs_ftp.request.chan;
    gcs_ftp.reply.sysid = gcs_ftp.request.sysid;
    gcs_ftp.reply.compid = gcs_ftp.request.compid;

    // sanity check the request size
    if (gcs_ftp.request.size > sizeof(gcs_ftp.request.data)) {
        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
        gcsmav_ftp_push_replies(gcsmav, &gcs_ftp.reply);
        return;
    }

    uint32_t now = time_millis();

    // check for session termination
    if (gcs_ftp.request.session != gcs_ftp.current_session &&
        (gcs_ftp.request.opcode == MavFtpOpTerminateSession || gcs_ftp.request.opcode == MavFtpOpResetSessions)) {
        // terminating a different session, just ack
        gcs_ftp.reply.opcode = MavFtpOpAck;
    } else if (gcs_ftp.fd != -1 && gcs_ftp.request.session != gcs_ftp.current_session &&
               now - gcs_ftp.last_send_ms < FTP_SESSION_TIMEOUT) {
        // if we have an open file and the session isn't right
        // then reject. This prevents IO on the wrong file
        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidSession);
    } else {
        if (gcs_ftp.fd != -1 &&
            gcs_ftp.request.session != gcs_ftp.current_session &&
            now - gcs_ftp.last_send_ms >= FTP_SESSION_TIMEOUT) {
            // if a new session appears and the old session has
            // been idle for more than the timeout then force
            // close the old session
            close(gcs_ftp.fd);
            gcs_ftp.fd = -1;
            gcs_ftp.current_session = -1;
        }
        
        // dispatch the command as needed
        switch (gcs_ftp.request.opcode) {
            case MavFtpOpNone:
                gcs_ftp.reply.opcode = MavFtpOpAck;
                break;
            case MavFtpOpTerminateSession:
            case MavFtpOpResetSessions:
                // we already handled this, just listed for completeness
                if (gcs_ftp.fd != -1) {
                    close(gcs_ftp.fd);
                    gcs_ftp.fd = -1;
                }
                gcs_ftp.current_session = -1;
                gcs_ftp.reply.opcode = MavFtpOpAck;
                break;
            case MavFtpOpListDirectory:
                gcsmav_ftp_list_dir(gcsmav, &gcs_ftp.request, &gcs_ftp.reply);
                break;
            case MavFtpOpOpenFileRO:
                {
                    // only allow one file to be open per session
                    if (gcs_ftp.fd != -1 && now - gcs_ftp.last_send_ms > FTP_SESSION_TIMEOUT) {
                        // no activity for 3s, assume client has
                        // timed out receiving open reply, close
                        // the file
                        close(gcs_ftp.fd);
                        gcs_ftp.fd = -1;
                        gcs_ftp.current_session = -1;
                    }
                    if (gcs_ftp.fd != -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFail);
                        break;
                    }

                    // sanity check that our the request looks well formed
                    const size_t file_name_len = rt_strnlen((char *)gcs_ftp.request.data, sizeof(gcs_ftp.request.data));
                    if ((file_name_len != gcs_ftp.request.size) || (gcs_ftp.request.size == 0)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
                        break;
                    }

                    gcs_ftp.request.data[sizeof(gcs_ftp.request.data) - 1] = 0; // ensure the path is null terminated

                    // get the file size
                    struct stat st;
                    if (stat((char *)gcs_ftp.request.data, &st)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }
                    const size_t file_size = st.st_size;

                    // actually open the file
                    gcs_ftp.fd = open((char *)gcs_ftp.request.data, O_RDONLY);
                    if (gcs_ftp.fd == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }
                    gcs_ftp.mode = MavFtpFileRead;
                    gcs_ftp.current_session = gcs_ftp.request.session;

                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    gcs_ftp.reply.size = sizeof(uint32_t);
                    put_le32_ptr(gcs_ftp.reply.data, (uint32_t)file_size);

                    // provide compatibility with old protocol banner download
                    if (rt_strncmp((const char *)gcs_ftp.request.data, "@PARAM/param.pck", 16) == 0) {
                        gcs_ftp.need_banner_send_mask |= 1U<<gcs_ftp.reply.chan;
                    }
                    break;
                }
            case MavFtpOpReadFile:
                {
                    // must actually be working on a file
                    if (gcs_ftp.fd == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFileNotFound);
                        break;
                    }

                    // must have the file in read mode
                    if ((gcs_ftp.mode != MavFtpFileRead)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFail);
                        break;
                    }

                    // seek to requested offset
                    if (lseek(gcs_ftp.fd, gcs_ftp.request.offset, SEEK_SET) == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    // fill the buffer
                    const ssize_t read_bytes = read(gcs_ftp.fd, gcs_ftp.reply.data, MIN(sizeof(gcs_ftp.reply.data),gcs_ftp.request.size));
                    if (read_bytes == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }
                    if (read_bytes == 0) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrEndOfFile);
                        break;
                    }

                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    gcs_ftp.reply.offset = gcs_ftp.request.offset;
                    gcs_ftp.reply.size = (uint8_t)read_bytes;
                    break;
                }
            case MavFtpOpAck:
            case MavFtpOpNack:
                // eat these, we just didn't expect them
                skip_push_reply = true;
                break;
            case MavFtpOpOpenFileWO:
            case MavFtpOpCreateFile:
                {
                    // only allow one file to be open per session
                    if (gcs_ftp.fd != -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFail);
                        break;
                    }

                    // sanity check that our the request looks well formed
                    const size_t file_name_len = rt_strnlen((char *)gcs_ftp.request.data, sizeof(gcs_ftp.request.data));
                    if ((file_name_len != gcs_ftp.request.size) || (gcs_ftp.request.size == 0)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
                        break;
                    }

                    gcs_ftp.request.data[sizeof(gcs_ftp.request.data) - 1] = 0; // ensure the path is null terminated

                    // actually open the file
                    gcs_ftp.fd = open((char *)gcs_ftp.request.data,
                                           (gcs_ftp.request.opcode == MavFtpOpCreateFile) ? O_WRONLY|O_CREAT|O_TRUNC : O_WRONLY);
                    if (gcs_ftp.fd == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }
                    gcs_ftp.mode = MavFtpFileWrite;
                    gcs_ftp.current_session = gcs_ftp.request.session;

                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    break;
                }
            case MavFtpOpWriteFile:
                {
                    // must actually be working on a file
                    if (gcs_ftp.fd == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFileNotFound);
                        break;
                    }

                    // must have the file in write mode
                    if ((gcs_ftp.mode != MavFtpFileWrite)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFail);
                        break;
                    }

                    // seek to requested offset
                    if (lseek(gcs_ftp.fd, gcs_ftp.request.offset, SEEK_SET) == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    // fill the buffer
                    const ssize_t write_bytes = write(gcs_ftp.fd, gcs_ftp.request.data, gcs_ftp.request.size);
                    if (write_bytes == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    gcs_ftp.reply.offset = gcs_ftp.request.offset;
                    break;
                }
            case MavFtpOpCreateDirectory:
                {
                    // sanity check that our the request looks well formed
                    const size_t file_name_len = rt_strnlen((char *)gcs_ftp.request.data, sizeof(gcs_ftp.request.data));
                    if ((file_name_len != gcs_ftp.request.size) || (gcs_ftp.request.size == 0)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
                        break;
                    }

                    gcs_ftp.request.data[sizeof(gcs_ftp.request.data) - 1] = 0; // ensure the path is null terminated

                    // actually make the directory
                    if (mkdir((char *)gcs_ftp.request.data, 0x777) == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    break;
                }
            case MavFtpOpRemoveDirectory:
            case MavFtpOpRemoveFile:
                {
                    // sanity check that our the request looks well formed
                    const size_t file_name_len = rt_strnlen((char *)gcs_ftp.request.data, sizeof(gcs_ftp.request.data));
                    if ((file_name_len != gcs_ftp.request.size) || (gcs_ftp.request.size == 0)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
                        break;
                    }

                    gcs_ftp.request.data[sizeof(gcs_ftp.request.data) - 1] = 0; // ensure the path is null terminated

                    // remove the file/dir
                    if (unlink((char *)gcs_ftp.request.data) == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    break;
                }
            case MavFtpOpCalcFileCRC32:
                {
                    // sanity check that our the request looks well formed
                    const size_t file_name_len = rt_strnlen((char *)gcs_ftp.request.data, sizeof(gcs_ftp.request.data));
                    if ((file_name_len != gcs_ftp.request.size) || (gcs_ftp.request.size == 0)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
                        break;
                    }

                    gcs_ftp.request.data[sizeof(gcs_ftp.request.data) - 1] = 0; // ensure the path is null terminated

                    // actually open the file
                    int fd = open((char *)gcs_ftp.request.data, O_RDONLY);
                    if (fd == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    uint32_t checksum = 0;
                    ssize_t read_size;
                    do {
                        read_size = read(fd, gcs_ftp.reply.data, sizeof(gcs_ftp.reply.data));
                        if (read_size == -1) {
                            gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                            break;
                        }
                        checksum = math_crc_crc32(checksum, gcs_ftp.reply.data, MIN((size_t)read_size, sizeof(gcs_ftp.reply.data)));
                    } while (read_size > 0);

                    close(fd);

                    // reset our scratch area so we don't leak data, and can leverage trimming
                    rt_memset(gcs_ftp.reply.data, 0, sizeof(gcs_ftp.reply.data));
                    gcs_ftp.reply.size = sizeof(uint32_t);
                    put_le32_ptr(gcs_ftp.reply.data, checksum);
                    gcs_ftp.reply.opcode = MavFtpOpAck;
                    break;
                }
            case MavFtpOpBurstReadFile:
                {
                    const uint16_t max_read = (gcs_ftp.request.size == 0?sizeof(gcs_ftp.reply.data):gcs_ftp.request.size);
                    // must actually be working on a file
                    if (gcs_ftp.fd == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFileNotFound);
                        break;
                    }

                    // must have the file in read mode
                    if ((gcs_ftp.mode != MavFtpFileRead)) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFail);
                        break;
                    }

                    // seek to requested offset
                    if (lseek(gcs_ftp.fd, gcs_ftp.request.offset, SEEK_SET) == -1) {
                        gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                        break;
                    }

                    /*
                      calculate a burst delay so that FTP burst
                      transfer doesn't use more than 1/3 of
                      available bandwidth on links that don't have
                      flow control. This reduces the chance of
                      lost packets a lot, which results in overall
                      faster transfers
                     */
                    uint32_t burst_delay_ms = 0;
                    if (mavcomm_valid_channel(gcs_ftp.request.chan)) {
                        rt_device_t port = mavlink_comm_port[gcs_ftp.request.chan];
                        if (port != NULL) { //if (port != NULL && port->get_flow_control() != AP_HAL::UARTDriver::FLOW_CONTROL_ENABLE) {
                            const uint32_t bw = SerialManager_bw_in_bytes_per_second(port);
                            const uint16_t pkt_size = MAV_PAYLOAD_SIZE(gcs_ftp.request.chan, FILE_TRANSFER_PROTOCOL) - (sizeof(gcs_ftp.reply.data) - max_read);
                            burst_delay_ms = 3000 * pkt_size / bw;
                        }
                    }

                    // this transfer size is enough for a full parameter file with max parameters
                    const uint32_t transfer_size = 500;
                    for (uint32_t i = 0; (i < transfer_size); i++) {
                        // fill the buffer
                        const ssize_t read_bytes = read(gcs_ftp.fd, gcs_ftp.reply.data, MIN(sizeof(gcs_ftp.reply.data), max_read));
                        if (read_bytes == -1) {
                            gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                            break;
                        }

                        if (read_bytes != sizeof(gcs_ftp.reply.data)) {
                            // don't send any old data
                            rt_memset(gcs_ftp.reply.data + read_bytes, 0, sizeof(gcs_ftp.reply.data) - read_bytes);
                        }

                        if (read_bytes == 0) {
                            gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrEndOfFile);
                            break;
                        }

                        gcs_ftp.reply.opcode = MavFtpOpAck;
                        gcs_ftp.reply.offset = gcs_ftp.request.offset + i * max_read;
                        gcs_ftp.reply.burst_complete = (i == (transfer_size - 1));
                        gcs_ftp.reply.size = (uint8_t)read_bytes;

                        gcsmav_ftp_push_replies(gcsmav, &gcs_ftp.reply);

                        if (read_bytes < max_read) {
                            // ensure the NACK which we send next is at the right offset
                            gcs_ftp.reply.offset += read_bytes;
                        }

                        // prep the reply to be used again
                        gcs_ftp.reply.seq_number++;

                        rt_thread_mdelay(burst_delay_ms);
                    }

                    if (gcs_ftp.reply.opcode != MavFtpOpNack) {
                        // prevent a duplicate packet send for
                        // normal replies of burst reads
                        skip_push_reply = true;
                    }
                    break;
                }

            case MavFtpOpRename: {
                // sanity check that the request looks well formed
                const char *filename1 = (char*)gcs_ftp.request.data;
                const size_t len1 = rt_strnlen(filename1, sizeof(gcs_ftp.request.data)-2);
                const char *filename2 = (char*)&gcs_ftp.request.data[len1+1];
                const size_t len2 = rt_strnlen(filename2, sizeof(gcs_ftp.request.data)-(len1+1));
                if (filename1[len1] != 0 || (len1+len2+1 != gcs_ftp.request.size) || (gcs_ftp.request.size == 0)) {
                    gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrInvalidDataSize);
                    break;
                }
                gcs_ftp.request.data[sizeof(gcs_ftp.request.data) - 1] = 0; // ensure the 2nd path is null terminated
                // remove the file/dir
                if (rename(filename1, filename2) != 0) {
                    gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFailErrno);
                    break;
                }
                gcs_ftp.reply.opcode = MavFtpOpAck;
                break;
            }

            case MavFtpOpTruncateFile:
            default:
                // this was bad data, just nack it
                gcs_send_text(MAV_SEVERITY_DEBUG, "Unsupported FTP: %d", (int)(gcs_ftp.request.opcode));
                gcsmav_ftp_error(gcsmav, &gcs_ftp.reply, MavFtpErrFail);
                break;
        }
    }

    if (!skip_push_reply) {
        gcsmav_ftp_push_replies(gcsmav, &gcs_ftp.reply);
    }
}

bool gcsmav_send_ftp_reply(gcs_mavlink_t gcsmav, const struct pending_ftp *reply)
{
    /*
      provide same banner we would give with old param download
    */
    if (gcs_ftp.need_banner_send_mask & (1U<<reply->chan)) {
        gcs_ftp.need_banner_send_mask &= ~(1U<<reply->chan);
        gcsmav_send_banner(gcsmav);
    }

    if (!MAV_HAVE_PAYLOAD_SPACE(gcsmav->chan, FILE_TRANSFER_PROTOCOL)) {
        return false;
    }

    uint8_t payload[251] = {0};
    put_le16_ptr(payload, reply->seq_number);
    payload[2] = reply->session;
    payload[3] = (uint8_t)(reply->opcode);
    payload[4] = reply->size;
    payload[5] = (uint8_t)(reply->req_opcode);
    payload[6] = reply->burst_complete ? 1 : 0;
    put_le32_ptr(&payload[8], reply->offset);
    memcpy(&payload[12], reply->data, sizeof(reply->data));
    mavlink_msg_file_transfer_protocol_send(
        reply->chan,
        0, reply->sysid, reply->compid,
        payload);
    if (reply->req_opcode == MavFtpOpTerminateSession) {
        gcs_ftp.last_send_ms = 0;
    } else {
        gcs_ftp.last_send_ms = time_millis();
    }
    return true;
}

void gcsmav_ftp_error(gcs_mavlink_t gcsmav, struct pending_ftp *response, enum MAV_FTP_ERROR error)
{
    response->opcode = MavFtpOpNack;
    response->data[0] = (uint8_t)(error);
    response->size = 1;

    // FIXME: errno's are not thread-local as they should be on ChibiOS
    if (error == MavFtpErrFailErrno) {
        // translate the errno's that we have useful messages for
        switch (errno) {
            case EEXIST:
                response->data[0] = (uint8_t)(MavFtpErrFileExists);
                break;
            case ENOENT:
                response->data[0] = (uint8_t)(MavFtpErrFileNotFound);
                break;
            default:
                response->data[1] = (uint8_t)(errno);
                response->size = 2;
                break;
        }
    }
}

// send our response back out to the system
void gcsmav_ftp_push_replies(gcs_mavlink_t gcsmav, struct pending_ftp *reply)
{
    while (!gcsmav_send_ftp_reply(gcsmav, reply)) {
        rt_thread_mdelay(2);
    }
}

// calculates how much string length is needed to fit this in a list response
int gcsmav_gen_dir_entry(char *dest, size_t space, const char *path, const struct dirent * entry)
{
    //const bool is_file = entry->d_type == DT_REG || entry->d_type == DT_LNK;
    const bool is_file = entry->d_type == DT_REG;

    if (space < 3) {
        return -1;
    }
    dest[0] = 0;

    if (!is_file && entry->d_type != DT_DIR) {
        return -1; // this just forces it so we can't send this back, it's easier then sending skips to a GCS
    }

    if (is_file) {
#ifdef MAX_NAME_LEN
        const uint8_t max_name_len = MIN(unsigned(MAX_NAME_LEN), 255U);
#else
        const uint8_t max_name_len = 255U;
#endif
        const size_t full_path_len = rt_strlen(path) + rt_strnlen(entry->d_name, max_name_len);
#if !defined(_MSC_VER)
        char full_path[max_name_len + 2];
#else
        char full_path[255 + 2];
#endif
        rt_snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
        struct stat st;
        if (stat(full_path, &st)) {
            return -1;
        }
        return rt_snprintf(dest, space, "F%s\t%u%c", entry->d_name, (unsigned)st.st_size, (char)0);
    } else {
        return rt_snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
    }
}

// list the contents of a directory, skip the offset number of entries before providing data
void gcsmav_ftp_list_dir(gcs_mavlink_t gcsmav, struct pending_ftp *request, struct pending_ftp *response)
{
    response->offset = request->offset; // this should be set for any failure condition for debugging

    const size_t directory_name_size = rt_strnlen((char *)request->data, sizeof(request->data));
    // sanity check that our the request looks well formed
    if ((directory_name_size != request->size) || (request->size == 0)) {
        gcsmav_ftp_error(gcsmav, response, MavFtpErrInvalidDataSize);
        return;
    }

    request->data[sizeof(request->data) - 1] = 0; // ensure the path is null terminated

    // open the dir
    DIR *dir = opendir((char *)request->data);
    if (dir == NULL) {
        gcsmav_ftp_error(gcsmav, response, MavFtpErrFailErrno);
        return;
    }

    // burn the entries we don't care about
    while (request->offset > 0) {
        const struct dirent *entry = readdir(dir);
        if(entry == NULL) {
            gcsmav_ftp_error(gcsmav, response, MavFtpErrEndOfFile);
            closedir(dir);
            return;
        }

        // check how much space would be needed to emit the listing
        const int needed_space = gcsmav_gen_dir_entry((char *)response->data, sizeof(request->data), (char *)request->data, entry);

        if (needed_space < 0 || needed_space > (int)sizeof(request->data)) {
            continue;
        }

        request->offset--;
    }

    // start packing in entries that fit
    uint8_t index = 0;
    struct dirent *entry;
    while ((entry = readdir(dir))) {
        // figure out if we can fit the file
        const int required_space = gcsmav_gen_dir_entry((char *)(response->data + index), sizeof(response->data) - index, (char *)request->data, entry);

        // couldn't ever send this so drop it
        if (required_space < 0) {
            continue;
        }

        // can't fit it in this one, leave it for the next list to send
        if ((required_space + index) >= (int)sizeof(request->data)) {
            break;
        }

        // step the index forward and keep going
        index += required_space + 1;
    }

    if (index == 0) {
        gcsmav_ftp_error(gcsmav, response, MavFtpErrEndOfFile);
        closedir(dir);
        return;
    }
    
    // strip any bad temp data from our response as it can confuse a GCS, and defeats 0 trimming
    if (index < sizeof(response->data)) {
        rt_memset(response->data + index, 0, MAX(0, (int)(sizeof(response->data)) - index));
    }

    response->opcode = MavFtpOpAck;
    response->size = index;

    closedir(dir);
}
/*------------------------------------test------------------------------------*/
#endif

